home *** CD-ROM | disk | FTP | other *** search
/ Enter 2006 September / Enter 09 2006.iso / Internet / SpamExperts Home 1.1 / SpamExperts Home.exe / lib / spamexperts.modules / spambayes / scripts / sb_server.pyc (.txt) < prev   
Encoding:
Python Compiled Bytecode  |  2006-07-14  |  33.2 KB  |  997 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.4)
  3.  
  4. '''The primary server for SpamBayes.
  5.  
  6. Currently serves the web interface, and any configured POP3 and SMTP
  7. proxies.
  8.  
  9. The POP3 proxy works with classifier.py, and adds a simple
  10. X-Spambayes-Classification header (ham/spam/unsure) to each incoming
  11. email.  You point the proxy at your POP3 server, and configure your
  12. email client to collect mail from the proxy then filter on the added
  13. header.  Usage:
  14.  
  15.     sb_server.py [options] [<server> [<server port>]]
  16.         <server> is the name of your real POP3 server
  17.         <port>   is the port number of your real POP3 server, which
  18.                  defaults to 110.
  19.  
  20.         options:
  21.             -h      : Displays this help message.
  22.             -d FILE : use the named DBM database file
  23.             -p FILE : the the named Pickle database file
  24.             -l port : proxy listens on this port number (default 110)
  25.             -u port : User interface listens on this port number
  26.                       (default 8880; Browse http://localhost:8880/)
  27.             -b      : Launch a web browser showing the user interface.
  28.  
  29.             -o section:option:value :
  30.                       set [section, option] in the options database
  31.                       to value
  32.  
  33.         All command line arguments and switches take their default
  34.         values from the [pop3proxy] and [html_ui] sections of
  35.         bayescustomize.ini.
  36.  
  37. For safety, and to help debugging, the whole POP3 conversation is
  38. written out to _pop3proxy.log for each run, if
  39. options["globals", "verbose"] is True.
  40.  
  41. To make rebuilding the database easier, uploaded messages are appended
  42. to _pop3proxyham.mbox and _pop3proxyspam.mbox.
  43. '''
  44. __author__ = 'Richie Hindle <richie@entrian.com>'
  45. __credits__ = 'Tim Peters, Neale Pickett, Tim Stone, all the Spambayes folk.'
  46.  
  47. try:
  48.     (True, False)
  49. except NameError:
  50.     (True, False) = (1, 0)
  51.  
  52.  
  53. try:
  54.     reversed
  55. except NameError:
  56.     
  57.     def reversed(seq):
  58.         seq = list(seq[:])
  59.         seq.reverse()
  60.         return iter(seq)
  61.  
  62.  
  63. todo = '\n\nWeb training interface:\n\nUser interface improvements:\n\n o Once the pieces are on separate pages, make the paste box bigger.\n o Deployment: Windows executable?  atlaxwin and ctypes?  Or just\n   webbrowser?\n o "Reload database" button.\n\n\nNew features:\n\n o Online manual.\n o Links to project homepage, mailing list, etc.\n o List of words with stats (it would have to be paged!) a la SpamSieve.\n\n\nCode quality:\n\n o Cope with the email client timing out and closing the connection.\n\n\nInfo:\n\n o Slightly-wordy index page; intro paragraph for each page.\n o In both stats and training results, report nham and nspam.\n o "Links" section (on homepage?) to project homepage, mailing list,\n   etc.\n\n\nGimmicks:\n\n o Classify a web page given a URL.\n o Graphs.  Of something.  Who cares what?\n o NNTP proxy.\n'
  64. import os
  65. import sys
  66. import re
  67. import errno
  68. import getopt
  69. import time
  70. import traceback
  71. import socket
  72. import cStringIO
  73. import email
  74. from thread import start_new_thread
  75. from email.Header import Header
  76. import spambayes.message as spambayes
  77. from spambayes import i18n
  78. from spambayes import Stats
  79. from spambayes import Dibbler
  80. from spambayes import storage
  81. from spambayes.FileCorpus import FileCorpus, ExpiryFileCorpus
  82. from spambayes.FileCorpus import FileMessageFactory, GzipFileMessageFactory
  83. from spambayes.Options import options, get_pathname_option, _
  84. from spambayes.UserInterface import UserInterfaceServer
  85. from spambayes.ProxyUI import ProxyUserInterface
  86. from spambayes.Version import get_current_version
  87. if sys.platform == 'darwin':
  88.     
  89.     try:
  90.         import resource
  91.     except ImportError:
  92.         pass
  93.  
  94.     (soft, hard) = resource.getrlimit(resource.RLIMIT_STACK)
  95.     newsoft = min(hard, max(soft, 1024 * 2048))
  96.     resource.setrlimit(resource.RLIMIT_STACK, (newsoft, hard))
  97.  
  98.  
  99. class AlreadyRunningException(Exception):
  100.     pass
  101.  
  102. HEADER_SIZE_FUDGE_FACTOR = 512
  103.  
  104. class ServerLineReader(Dibbler.BrighterAsyncChat):
  105.     """An async socket that reads lines from a remote server and
  106.     simply calls a callback with the data.  The BayesProxy object
  107.     can't connect to the real POP3 server and talk to it
  108.     synchronously, because that would block the process."""
  109.     
  110.     def __init__(self, serverName, serverPort, lineCallback, ssl = False, map = None):
  111.         Dibbler.BrighterAsyncChat.__init__(self, map = map)
  112.         self.lineCallback = lineCallback
  113.         self.handled_exception = False
  114.         self.request = ''
  115.         self.set_terminator('\r\n')
  116.         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
  117.         self.socket.setblocking(1)
  118.         
  119.         try:
  120.             self.connect((serverName, serverPort))
  121.         except socket.error:
  122.             e = None
  123.             error = "Can't connect to %s:%d: %s" % (serverName, serverPort, e)
  124.             now = time.time()
  125.             then = time.time() - 3600
  126.             if error not in state.reported_errors and options[('globals', 'verbose')] or state.reported_errors[error] < then:
  127.                 print >>sys.stderr, error
  128.                 state.reported_errors[error] = now
  129.             
  130.             self.lineCallback('-ERR %s\r\n' % error)
  131.             self.lineCallback('')
  132.             self.close()
  133.  
  134.         if ssl:
  135.             
  136.             try:
  137.                 self.ssl_socket = socket.ssl(self.socket)
  138.             except socket.sslerror:
  139.                 why = None
  140.                 if why[0] == 1:
  141.                     print >>sys.stderr, "Can't use SSL"
  142.                 else:
  143.                     raise 
  144.             except:
  145.                 why[0] == 1
  146.  
  147.             self.send = self.send_ssl
  148.             self.recv = self.recv_ssl
  149.         
  150.         self.socket.setblocking(0)
  151.  
  152.     
  153.     def send_ssl(self, data):
  154.         return self.ssl_socket.write(data)
  155.  
  156.     
  157.     def handle_expt(self):
  158.         if not self.handled_exception:
  159.             print >>sys.stderr, 'Unhandled exception in ServerLineReader'
  160.             self.handled_exception = True
  161.         
  162.  
  163.     
  164.     def recv_ssl(self, buffer_size):
  165.         
  166.         try:
  167.             data = self.ssl_socket.read(buffer_size)
  168.             if not data:
  169.                 self.handle_close()
  170.                 return ''
  171.             else:
  172.                 return data
  173.         except socket.sslerror:
  174.             why = None
  175.             if why[0] == 6:
  176.                 self.handle_close()
  177.                 return ''
  178.             elif why[0] == 5:
  179.                 self.handle_close()
  180.                 return ''
  181.             elif why[0] == 2:
  182.                 return ''
  183.             else:
  184.                 raise 
  185.         except:
  186.             why[0] == 6
  187.  
  188.  
  189.     
  190.     def collect_incoming_data(self, data):
  191.         self.request = self.request + data
  192.  
  193.     
  194.     def found_terminator(self):
  195.         self.lineCallback(self.request + '\r\n')
  196.         self.request = ''
  197.  
  198.     
  199.     def handle_close(self):
  200.         self.lineCallback('')
  201.         self.close()
  202.         
  203.         try:
  204.             del self.ssl_socket
  205.         except AttributeError:
  206.             pass
  207.  
  208.  
  209.  
  210.  
  211. class POP3ProxyBase(Dibbler.BrighterAsyncChat):
  212.     """An async dispatcher that understands POP3 and proxies to a POP3
  213.     server, calling `self.onTransaction(request, response)` for each
  214.     transaction. Responses are not un-byte-stuffed before reaching
  215.     self.onTransaction() (they probably should be for a totally generic
  216.     POP3ProxyBase class, but BayesProxy doesn't need it and it would
  217.     mean re-stuffing them afterwards).  self.onTransaction() should
  218.     return the response to pass back to the email client - the response
  219.     can be the verbatim response or a processed version of it.  The
  220.     special command 'KILL' kills it (passing a 'QUIT' command to the
  221.     server).
  222.     """
  223.     
  224.     def __init__(self, clientSocket, serverName, serverPort, ssl = False, map = Dibbler._defaultContext._map):
  225.         Dibbler.BrighterAsyncChat.__init__(self, clientSocket)
  226.         self.request = ''
  227.         self.response = ''
  228.         self.set_terminator('\r\n')
  229.         self.command = ''
  230.         self.args = []
  231.         self.isClosing = False
  232.         self.seenAllHeaders = False
  233.         self.startTime = 0
  234.         if not self.onIncomingConnection(clientSocket):
  235.             self.push('-ERR Connection not allowed\r\n')
  236.             self.close_when_done()
  237.             return None
  238.         
  239.         self.serverSocket = ServerLineReader(serverName, serverPort, self.onServerLine, ssl, map)
  240.  
  241.     
  242.     def onIncomingConnection(self, clientSocket):
  243.         '''Checks the security settings.'''
  244.         remoteIP = clientSocket.getpeername()[0]
  245.         trustedIPs = options[('pop3proxy', 'allow_remote_connections')]
  246.         if trustedIPs == '*' or remoteIP == clientSocket.getsockname()[0]:
  247.             return True
  248.         
  249.         trustedIPs = trustedIPs.replace('.', '\\.').replace('*', '([01]?\\d\\d?|2[04]\\d|25[0-5])')
  250.         for trusted in trustedIPs.split(','):
  251.             if re.search('^' + trusted + '$', remoteIP):
  252.                 return True
  253.                 continue
  254.         
  255.         return False
  256.  
  257.     
  258.     def onTransaction(self, command, args, response):
  259.         '''Overide this.  Takes the raw request and the response, and
  260.         returns the (possibly processed) response to pass back to the
  261.         email client.
  262.         '''
  263.         raise NotImplementedError
  264.  
  265.     
  266.     def onServerLine(self, line):
  267.         '''A line of response has been received from the POP3 server.'''
  268.         isFirstLine = not (self.response)
  269.         self.response = self.response + line
  270.         if not self.seenAllHeaders:
  271.             pass
  272.         self.seenAllHeaders = line in [
  273.             '\r\n',
  274.             '\n']
  275.         if not line:
  276.             self.isClosing = True
  277.         
  278.         if not self.command:
  279.             self.push(self.response)
  280.             self.response = ''
  281.         
  282.         if self.command in [
  283.             'TOP',
  284.             'RETR'] and self.seenAllHeaders and time.time() > self.startTime + options[('pop3proxy', 'retrieval_timeout')]:
  285.             self.onResponse()
  286.             self.response = ''
  287.         elif (not self.isMultiline() and line == '.\r\n' or isFirstLine) and line.startswith('-ERR'):
  288.             self.onResponse()
  289.             self.response = ''
  290.         
  291.  
  292.     
  293.     def isMultiline(self):
  294.         '''Returns True if the request should get a multiline
  295.         response (assuming the response is positive).
  296.         '''
  297.         if self.command in [
  298.             'USER',
  299.             'PASS',
  300.             'APOP',
  301.             'QUIT',
  302.             'STAT',
  303.             'DELE',
  304.             'NOOP',
  305.             'RSET',
  306.             'KILL']:
  307.             return False
  308.         elif self.command in [
  309.             'RETR',
  310.             'TOP',
  311.             'CAPA']:
  312.             return True
  313.         elif self.command in [
  314.             'LIST',
  315.             'UIDL']:
  316.             return len(self.args) == 0
  317.         else:
  318.             return False
  319.  
  320.     
  321.     def collect_incoming_data(self, data):
  322.         '''Asynchat override.'''
  323.         self.request = self.request + data
  324.  
  325.     
  326.     def found_terminator(self):
  327.         '''Asynchat override.'''
  328.         verb = self.request.strip().upper()
  329.         if verb == 'KILL':
  330.             self.socket.shutdown(2)
  331.             self.close()
  332.             raise SystemExit
  333.         elif verb == 'CRASH':
  334.             x = 0
  335.             y = 1 / x
  336.         
  337.         self.serverSocket.push(self.request + '\r\n')
  338.         if self.request.strip() == '':
  339.             self.command = ''
  340.             self.args = []
  341.         else:
  342.             splitCommand = self.request.strip().split()
  343.             self.command = splitCommand[0].upper()
  344.             self.args = splitCommand[1:]
  345.             self.startTime = time.time()
  346.         self.request = ''
  347.  
  348.     
  349.     def onResponse(self):
  350.         for unsupported in [
  351.             'PIPELINING',
  352.             'STLS']:
  353.             unsupportedLine = '(?im)^%s[^\\n]*\\n' % (unsupported,)
  354.             self.response = re.sub(unsupportedLine, '', self.response)
  355.         
  356.         if self.response:
  357.             cooked = self.onTransaction(self.command, self.args, self.response)
  358.             self.push(cooked)
  359.         
  360.         if self.isClosing:
  361.             self.close_when_done()
  362.         
  363.         self.command = ''
  364.         self.args = []
  365.         self.isClosing = False
  366.         self.seenAllHeaders = False
  367.  
  368.  
  369.  
  370. class BayesProxyListener(Dibbler.Listener):
  371.     '''Listens for incoming email client connections and spins off
  372.     BayesProxy objects to serve them.
  373.     '''
  374.     
  375.     def __init__(self, serverName, serverPort, proxyPort, ssl = False):
  376.         proxyArgs = (serverName, serverPort, ssl)
  377.         Dibbler.Listener.__init__(self, proxyPort, BayesProxy, proxyArgs)
  378.         print 'Listener on port %s is proxying %s:%d' % (_addressPortStr(proxyPort), serverName, serverPort)
  379.  
  380.  
  381.  
  382. class BayesProxy(POP3ProxyBase):
  383.     """Proxies between an email client and a POP3 server, inserting
  384.     judgement headers.  It acts on the following POP3 commands:
  385.  
  386.      o STAT:
  387.         o Adds the size of all the judgement headers to the maildrop
  388.           size.
  389.  
  390.      o LIST:
  391.         o With no message number: adds the size of an judgement header
  392.           to the message size for each message in the scan listing.
  393.         o With a message number: adds the size of an judgement header
  394.           to the message size.
  395.  
  396.      o RETR:
  397.         o Adds the judgement header based on the raw headers and body
  398.           of the message.
  399.  
  400.      o TOP:
  401.         o Adds the judgement header based on the raw headers and as
  402.           much of the body as the TOP command retrieves.  This can
  403.           mean that the header might have a different value for
  404.           different calls to TOP, or for calls to TOP vs. calls to
  405.           RETR.  I'm assuming that the email client will either not
  406.           make multiple calls, or will cope with the headers being
  407.           different.
  408.  
  409.      o USER:
  410.         o Does no processing based on the USER command itself, but
  411.           expires any old messages in the three caches.
  412.     """
  413.     
  414.     def __init__(self, clientSocket, serverName, serverPort, ssl = False):
  415.         POP3ProxyBase.__init__(self, clientSocket, serverName, serverPort, ssl)
  416.         self.handlers = {
  417.             'STAT': self.onStat,
  418.             'LIST': self.onList,
  419.             'RETR': self.onRetr,
  420.             'TOP': self.onTop,
  421.             'USER': self.onUser }
  422.         state.totalSessions += 1
  423.         state.activeSessions += 1
  424.         self.isClosed = False
  425.  
  426.     
  427.     def send(self, data):
  428.         '''Logs the data to the log file.'''
  429.         if options[('globals', 'verbose')]:
  430.             state.logFile.write(data)
  431.             state.logFile.flush()
  432.         
  433.         
  434.         try:
  435.             return POP3ProxyBase.send(self, data)
  436.         except socket.error:
  437.             self.close()
  438.  
  439.  
  440.     
  441.     def recv(self, size):
  442.         '''Logs the data to the log file.'''
  443.         data = POP3ProxyBase.recv(self, size)
  444.         if options[('globals', 'verbose')]:
  445.             state.logFile.write(data)
  446.             state.logFile.flush()
  447.         
  448.         return data
  449.  
  450.     
  451.     def close(self):
  452.         if not self.isClosed:
  453.             self.isClosed = True
  454.             state.activeSessions -= 1
  455.             POP3ProxyBase.close(self)
  456.         
  457.  
  458.     
  459.     def onTransaction(self, command, args, response):
  460.         '''Takes the raw request and response, and returns the
  461.         (possibly processed) response to pass back to the email client.
  462.         '''
  463.         handler = self.handlers.get(command, self.onUnknown)
  464.         return handler(command, args, response)
  465.  
  466.     
  467.     def onStat(self, command, args, response):
  468.         '''Adds the size of all the judgement headers to the maildrop
  469.         size.'''
  470.         match = re.search('^\\+OK\\s+(\\d+)\\s+(\\d+)(.*)\\r\\n', response)
  471.         if match:
  472.             count = int(match.group(1))
  473.             size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR * count
  474.             return '+OK %d %d%s\r\n' % (count, size, match.group(3))
  475.         else:
  476.             return response
  477.  
  478.     
  479.     def onList(self, command, args, response):
  480.         '''Adds the size of an judgement header to the message
  481.         size(s).'''
  482.         if response.count('\r\n') > 1:
  483.             lines = response.split('\r\n')
  484.             outputLines = [
  485.                 lines[0]]
  486.             for line in lines[1:]:
  487.                 match = re.search('^(\\d+)\\s+(\\d+)', line)
  488.                 if match:
  489.                     number = int(match.group(1))
  490.                     size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR
  491.                     line = '%d %d' % (number, size)
  492.                 
  493.                 outputLines.append(line)
  494.             
  495.             return '\r\n'.join(outputLines)
  496.         else:
  497.             match = re.search('^\\+OK\\s+(\\d+)\\s+(\\d+)(.*)\\r\\n', response)
  498.             if match:
  499.                 messageNumber = match.group(1)
  500.                 size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR
  501.                 trailer = match.group(3)
  502.                 return '+OK %s %s%s\r\n' % (messageNumber, size, trailer)
  503.             else:
  504.                 return response
  505.  
  506.     
  507.     def onRetr(self, command, args, response):
  508.         '''Adds the judgement header based on the raw headers and body
  509.         of the message.'''
  510.         terminatingDotPresent = response[-4:] == '\n.\r\n'
  511.         if terminatingDotPresent:
  512.             response = response[:-3]
  513.         
  514.         (statusLine, messageText) = response.split('\n', 1)
  515.         statusData = statusLine.split()
  516.         ok = statusData[0]
  517.         if ok.strip().upper() != '+OK':
  518.             return response
  519.         
  520.         
  521.         try:
  522.             msg = email.message_from_string(messageText, _class = spambayes.message.SBHeaderMessage)
  523.             msg.setId(state.getNewMessageName())
  524.             (prob, clues) = state.bayes.spamprob(msg.tokenize(), evidence = True)
  525.             msg.addSBHeaders(prob, clues)
  526.             if (command == 'RETR' or command == 'TOP') and len(args) == 2 and args[1] == '99999999':
  527.                 cls = msg.GetClassification()
  528.                 state.RecordClassification(cls, prob)
  529.                 if cls == options[('Headers', 'header_ham_string')] and options[('Storage', 'no_cache_bulk_ham')]:
  530.                     pass
  531.                 isSuppressedBulkHam = msg.get('precedence') in [
  532.                     'bulk',
  533.                     'list']
  534.                 size_limit = options[('Storage', 'no_cache_large_messages')]
  535.                 if size_limit > 0:
  536.                     pass
  537.                 isTooBig = len(messageText) > size_limit
  538.                 if not (state.isTest) and options[('Storage', 'cache_messages')] and not isSuppressedBulkHam and not isTooBig:
  539.                     makeMessage = state.unknownCorpus.makeMessage
  540.                     message = makeMessage(msg.getId(), msg.as_string())
  541.                     state.unknownCorpus.addMessage(message)
  542.                 
  543.             
  544.             headers = []
  545.             for name, value in msg.items():
  546.                 header = '%s: %s' % (name, value)
  547.                 headers.append(re.sub('\\r?\\n', '\r\n', header))
  548.             
  549.             
  550.             try:
  551.                 body = re.split('\\n\\r?\\n', messageText, 1)[1]
  552.             except IndexError:
  553.                 messageText = '\r\n'.join(headers) + '\r\n\r\n'
  554.  
  555.             messageText = '\r\n'.join(headers) + '\r\n\r\n' + body
  556.         except:
  557.             (messageText, details) = spambayes.message.insert_exception_header(messageText)
  558.             print >>sys.stderr, details
  559.  
  560.         retval = ok + '\n' + messageText
  561.         if terminatingDotPresent:
  562.             retval += '.\r\n'
  563.         
  564.         return retval
  565.  
  566.     
  567.     def onTop(self, command, args, response):
  568.         '''Adds the judgement header based on the raw headers and as
  569.         much of the body as the TOP command retrieves.'''
  570.         return self.onRetr(command, args, response)
  571.  
  572.     
  573.     def onUser(self, command, args, response):
  574.         '''Spins off three separate threads that expires any old messages
  575.         in the three caches, but does not do any processing of the USER
  576.         command itself.'''
  577.         start_new_thread(state.spamCorpus.removeExpiredMessages, ())
  578.         start_new_thread(state.hamCorpus.removeExpiredMessages, ())
  579.         start_new_thread(state.unknownCorpus.removeExpiredMessages, ())
  580.         return response
  581.  
  582.     
  583.     def onUnknown(self, command, args, response):
  584.         """Default handler; returns the server's response verbatim."""
  585.         return response
  586.  
  587.  
  588.  
  589. def open_platform_mutex(mutex_name = 'SpamBayesServer'):
  590.     if sys.platform.startswith('win'):
  591.         
  592.         try:
  593.             import win32event
  594.             import win32api
  595.             import winerror
  596.             import win32con
  597.             import pywintypes
  598.             import ntsecuritycon
  599.             
  600.             try:
  601.                 hmutex = win32event.CreateMutex(None, True, mutex_name)
  602.             except win32event.error:
  603.                 details = None
  604.                 if details[0] != winerror.ERROR_ACCESS_DENIED:
  605.                     raise 
  606.                 
  607.                 raise AlreadyRunningException
  608.  
  609.             if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
  610.                 win32api.CloseHandle(hmutex)
  611.                 raise AlreadyRunningException
  612.             
  613.             return hmutex
  614.         except ImportError:
  615.             pass
  616.         except:
  617.             None<EXCEPTION MATCH>ImportError
  618.         
  619.  
  620.     None<EXCEPTION MATCH>ImportError
  621.  
  622.  
  623. def close_platform_mutex(mutex):
  624.     if sys.platform.startswith('win'):
  625.         if mutex is not None:
  626.             mutex.Close()
  627.         
  628.     
  629.  
  630.  
  631. class State:
  632.     
  633.     def __init__(self):
  634.         '''Initialises the State object that holds the state of the app.
  635.         The default settings are read from Options.py and bayescustomize.ini
  636.         and are then overridden by the command-line processing code in the
  637.         __main__ code below.'''
  638.         self.logFile = None
  639.         self.bayes = None
  640.         self.platform_mutex = None
  641.         self.prepared = False
  642.         self.can_stop = True
  643.         self.init()
  644.         self.uiPort = options[('html_ui', 'port')]
  645.         self.launchUI = options[('html_ui', 'launch_browser')]
  646.         self.gzipCache = options[('Storage', 'cache_use_gzip')]
  647.         self.cacheExpiryDays = options[('Storage', 'cache_expiry_days')]
  648.         self.runTestServer = False
  649.         self.isTest = False
  650.  
  651.     
  652.     def init(self):
  653.         if not not (self.prepared):
  654.             raise AssertionError, 'init after prepare, but before close'
  655.         self.lang_manager = i18n.LanguageManager()
  656.         self.lang_manager.set_language(self.lang_manager.locale_default_lang())
  657.         for language in reversed(options[('globals', 'language')]):
  658.             self.lang_manager.add_language(language)
  659.         
  660.         if options[('globals', 'verbose')]:
  661.             print 'Asked to add languages: ' + ', '.join(options[('globals', 'language')])
  662.             print 'Set language to ' + str(self.lang_manager.current_langs_codes)
  663.         
  664.         if options[('globals', 'verbose')]:
  665.             self.logFile = open('_pop3proxy.log', 'wb', 0)
  666.         
  667.         if not hasattr(self, 'servers'):
  668.             self.servers = []
  669.             if options[('pop3proxy', 'remote_servers')]:
  670.                 for server in options[('pop3proxy', 'remote_servers')]:
  671.                     server = server.strip()
  672.                     if server.find(':') > -1:
  673.                         (server, port) = server.split(':', 1)
  674.                     else:
  675.                         port = '110'
  676.                     self.servers.append((server, int(port)))
  677.                 
  678.             
  679.         
  680.         if not hasattr(self, 'proxyPorts'):
  681.             self.proxyPorts = []
  682.             if options[('pop3proxy', 'listen_ports')]:
  683.                 splitPorts = options[('pop3proxy', 'listen_ports')]
  684.                 self.proxyPorts = map(_addressAndPort, splitPorts)
  685.             
  686.         
  687.         if len(self.servers) != len(self.proxyPorts):
  688.             print 'pop3proxy_servers & pop3proxy_ports are different lengths!'
  689.             sys.exit()
  690.         
  691.         self.reported_errors = { }
  692.         self.totalSessions = 0
  693.         self.activeSessions = 0
  694.         self.numSpams = 0
  695.         self.numHams = 0
  696.         self.numUnsure = 0
  697.         self.lastBaseMessageName = ''
  698.         self.uniquifier = 2
  699.  
  700.     
  701.     def close(self):
  702.         if not self.prepared:
  703.             raise AssertionError, 'closed without being prepared!'
  704.         self.servers = None
  705.         if self.bayes is not None:
  706.             if self.bayes.nham != 0 and self.bayes.nspam != 0:
  707.                 state.bayes.store()
  708.             
  709.             self.bayes.close()
  710.             self.bayes = None
  711.         
  712.         self.spamCorpus = None
  713.         self.hamCorpus = None
  714.         self.unknownCorpus = None
  715.         self.spamTrainer = None
  716.         self.hamTrainer = None
  717.         self.prepared = False
  718.         close_platform_mutex(self.platform_mutex)
  719.         self.platform_mutex = None
  720.  
  721.     
  722.     def prepare(self, can_stop = True):
  723.         '''Do whatever needs to be done to prepare for running.  If
  724.         can_stop is False, then we may not let the user shut down the
  725.         proxy - for example, running as a Windows service this should
  726.         be the case.'''
  727.         if not self.platform_mutex is None:
  728.             raise AssertionError, 'Should not already have the mutex'
  729.         self.platform_mutex = open_platform_mutex()
  730.         self.can_stop = can_stop
  731.         self.createWorkers()
  732.         self.prepared = True
  733.  
  734.     
  735.     def buildServerStrings(self):
  736.         '''After the server details have been set up, this creates string
  737.         versions of the details, for display in the Status panel.'''
  738.         serverStrings = [ '%s:%s' % (s, p) for s, p in self.servers ]
  739.         self.serversString = ', '.join(serverStrings)
  740.         self.proxyPortsString = ', '.join(map(_addressPortStr, self.proxyPorts))
  741.  
  742.     
  743.     def buildStatusStrings(self):
  744.         '''Build the status message(s) to display on the home page of the
  745.         web interface.'''
  746.         nspam = self.bayes.nspam
  747.         nham = self.bayes.nham
  748.         if nspam > 10 and nham > 10:
  749.             db_ratio = nham / float(nspam)
  750.             big = None
  751.             small = None
  752.             if db_ratio > 5.0:
  753.                 self.warning = _('Warning: you have much more ham than spam - SpamBayes works best with approximately even numbers of ham and spam.')
  754.             elif db_ratio < 1 / 5.0:
  755.                 self.warning = _('Warning: you have much more spam than ham - SpamBayes works best with approximately even numbers of ham and spam.')
  756.             else:
  757.                 self.warning = ''
  758.         elif nspam > 0 or nham > 0:
  759.             self.warning = _('Database only has %d good and %d spam - you should consider performing additional training.') % (nham, nspam)
  760.         else:
  761.             self.warning = _("Database has no training information.  SpamBayes will classify all messages as 'unsure', ready for you to train.")
  762.         spam_cut = options[('Categorization', 'spam_cutoff')]
  763.         ham_cut = options[('Categorization', 'ham_cutoff')]
  764.         if spam_cut < 0.5:
  765.             self.warning += _('<br/>Warning: we do not recommend setting the spam threshold less than 0.5.')
  766.         
  767.         if ham_cut > 0.5:
  768.             self.warning += _('<br/>Warning: we do not recommend setting the ham threshold greater than 0.5.')
  769.         
  770.         if ham_cut > spam_cut:
  771.             self.warning += _('<br/>Warning: your ham threshold is <b>higher</b> than your spam threshold. Results are unpredictable.')
  772.         
  773.  
  774.     
  775.     def createWorkers(self):
  776.         '''Using the options that were initialised in __init__ and then
  777.         possibly overridden by the driver code, create the Bayes object,
  778.         the Corpuses, the Trainers and so on.'''
  779.         print 'Loading database...',
  780.         if self.isTest:
  781.             self.useDB = 'pickle'
  782.             self.DBName = '_pop3proxy_test.pickle'
  783.         
  784.         if not hasattr(self, 'DBName'):
  785.             (self.DBName, self.useDB) = storage.database_type([])
  786.         
  787.         self.bayes = storage.open_storage(self.DBName, self.useDB)
  788.         if not hasattr(self, 'MBDName'):
  789.             (self.MDBName, self.useMDB) = spambayes.message.database_type()
  790.         
  791.         self.mdb = spambayes.message.open_storage(self.MDBName, self.useMDB)
  792.         self.stats = Stats.Stats(options, self.mdb)
  793.         self.buildStatusStrings()
  794.         if not self.isTest:
  795.             sc = get_pathname_option('Storage', 'spam_cache')
  796.             hc = get_pathname_option('Storage', 'ham_cache')
  797.             uc = get_pathname_option('Storage', 'unknown_cache')
  798.             map(storage.ensureDir, [
  799.                 sc,
  800.                 hc,
  801.                 uc])
  802.             if self.gzipCache:
  803.                 factory = GzipFileMessageFactory()
  804.             else:
  805.                 factory = FileMessageFactory()
  806.             age = options[('Storage', 'cache_expiry_days')] * 24 * 60 * 60
  807.             self.spamCorpus = ExpiryFileCorpus(age, factory, sc, '[0123456789\\-]*', cacheSize = 20)
  808.             self.hamCorpus = ExpiryFileCorpus(age, factory, hc, '[0123456789\\-]*', cacheSize = 20)
  809.             self.unknownCorpus = ExpiryFileCorpus(age, factory, uc, '[0123456789\\-]*', cacheSize = 20)
  810.             self.spamCorpus.removeExpiredMessages()
  811.             self.hamCorpus.removeExpiredMessages()
  812.             self.unknownCorpus.removeExpiredMessages()
  813.             self.spamTrainer = storage.SpamTrainer(self.bayes)
  814.             self.hamTrainer = storage.HamTrainer(self.bayes)
  815.             self.spamCorpus.addObserver(self.spamTrainer)
  816.             self.hamCorpus.addObserver(self.hamTrainer)
  817.         
  818.  
  819.     
  820.     def getNewMessageName(self):
  821.         messageName = '%10.10d' % long(time.time())
  822.         if messageName == self.lastBaseMessageName:
  823.             messageName = '%s-%d' % (messageName, self.uniquifier)
  824.             self.uniquifier += 1
  825.         else:
  826.             self.lastBaseMessageName = messageName
  827.             self.uniquifier = 2
  828.         return messageName
  829.  
  830.     
  831.     def RecordClassification(self, cls, score):
  832.         '''Record the classification in the session statistics.
  833.  
  834.         cls should match one of the options["Headers", "header_*_string"]
  835.         values.
  836.  
  837.         score is the score the message received.        
  838.         '''
  839.         if cls == options[('Headers', 'header_ham_string')]:
  840.             self.numHams += 1
  841.         elif cls == options[('Headers', 'header_spam_string')]:
  842.             self.numSpams += 1
  843.         else:
  844.             self.numUnsure += 1
  845.         self.stats.RecordClassification(score)
  846.  
  847.  
  848.  
  849. def _addressAndPort(s):
  850.     '''Decode a string representing a port to bind to, with optional address.'''
  851.     s = s.strip()
  852.     if ':' in s:
  853.         (addr, port) = s.split(':')
  854.         return (addr, int(port))
  855.     else:
  856.         return ('', int(s))
  857.  
  858.  
  859. def _addressPortStr(.0):
  860.     '''Encode a string representing a port to bind to, with optional address.'''
  861.     (addr, port) = .0
  862.     if not addr:
  863.         return str(port)
  864.     else:
  865.         return '%s:%d' % (addr, port)
  866.  
  867. state = State()
  868. proxyListeners = []
  869.  
  870. def _createProxies(servers, proxyPorts):
  871.     '''Create BayesProxyListeners for all the given servers.'''
  872.     for server, serverPort in zip(servers, proxyPorts):
  873.         proxyPort = None
  874.         ssl = options[('pop3proxy', 'use_ssl')]
  875.         if ssl == 'automatic':
  876.             ssl = serverPort == 995
  877.         
  878.         listener = BayesProxyListener(server, serverPort, proxyPort, ssl)
  879.         proxyListeners.append(listener)
  880.     
  881.  
  882.  
  883. def _recreateState():
  884.     global state
  885.     for proxy in proxyListeners:
  886.         proxy.close()
  887.     
  888.     del proxyListeners[:]
  889.     state.close()
  890.     state = State()
  891.     prepare()
  892.     _createProxies(state.servers, state.proxyPorts)
  893.     return state
  894.  
  895.  
  896. def main(servers, proxyPorts, uiPort, launchUI):
  897.     """Runs the proxy forever or until a 'KILL' command is received or
  898.     someone hits Ctrl+Break."""
  899.     _createProxies(servers, proxyPorts)
  900.     httpServer = UserInterfaceServer(uiPort)
  901.     proxyUI = ProxyUserInterface(state, _recreateState)
  902.     httpServer.register(proxyUI)
  903.     Dibbler.run(launchBrowser = launchUI)
  904.  
  905.  
  906. def prepare(can_stop = True):
  907.     state.init()
  908.     state.prepare(can_stop)
  909.     smtpproxy = smtpproxy
  910.     import spambayes
  911.     (servers, proxyPorts) = smtpproxy.LoadServerInfo()
  912.     proxyListeners.extend(smtpproxy.CreateProxies(servers, proxyPorts, smtpproxy.SMTPTrainer(state.bayes, state)))
  913.     state.buildServerStrings()
  914.  
  915.  
  916. def start():
  917.     if not state.prepared:
  918.         raise AssertionError, 'starting before preparing state'
  919.     
  920.     try:
  921.         main(state.servers, state.proxyPorts, state.uiPort, state.launchUI)
  922.     finally:
  923.         state.close()
  924.  
  925.  
  926.  
  927. def stop():
  928.     urlopen = urlopen
  929.     urlencode = urlencode
  930.     import urllib
  931.     urlopen('http://localhost:%d/save' % state.uiPort, urlencode({
  932.         'how': _('Save & shutdown') })).read()
  933.  
  934.  
  935. def run():
  936.     
  937.     try:
  938.         (opts, args) = getopt.getopt(sys.argv[1:], 'hbd:p:l:u:o:')
  939.     except getopt.error:
  940.         msg = None
  941.         print >>sys.stderr, str(msg) + '\n\n' + __doc__
  942.         sys.exit()
  943.  
  944.     runSelfTest = False
  945.     for opt, arg in opts:
  946.         if opt == '-h':
  947.             print >>sys.stderr, __doc__
  948.             sys.exit()
  949.             continue
  950.         if opt == '-b':
  951.             state.launchUI = True
  952.             continue
  953.         if opt == '-l':
  954.             state.proxyPorts = [ _addressAndPort(a) for a in arg.split(',') ]
  955.             continue
  956.         []
  957.         if opt == '-u':
  958.             state.uiPort = int(arg)
  959.             continue
  960.         []
  961.         if opt == '-o':
  962.             options.set_from_cmdline(arg, sys.stderr)
  963.             continue
  964.     
  965.     (state.DBName, state.useDB) = storage.database_type(opts)
  966.     v = get_current_version()
  967.     print '%s\n' % (v.get_long_version('SpamBayes POP3 Proxy'),)
  968.     if len(args) <= len(args):
  969.         pass
  970.     elif len(args) <= 2:
  971.         if len(args) == 1:
  972.             state.servers = [
  973.                 (args[0], 110)]
  974.         elif len(args) == 2:
  975.             state.servers = [
  976.                 (args[0], int(args[1]))]
  977.         
  978.         if len(args) > 0 and state.proxyPorts == []:
  979.             state.proxyPorts = [
  980.                 ('', 110)]
  981.         
  982.         
  983.         try:
  984.             prepare()
  985.         except AlreadyRunningException:
  986.             print >>sys.stderr, 'ERROR: The proxy is already running on this machine.'
  987.             print >>sys.stderr, 'Please stop the existing proxy and try again'
  988.             return None
  989.  
  990.         start()
  991.     else:
  992.         print >>sys.stderr, __doc__
  993.  
  994. if __name__ == '__main__':
  995.     run()
  996.  
  997.